package com.pic.mgt.monitor.physicalmonitor;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import name.pachler.nio.file.ClosedWatchServiceException;
import name.pachler.nio.file.FileSystems;
import name.pachler.nio.file.Path;
import name.pachler.nio.file.Paths;
import name.pachler.nio.file.StandardWatchEventKind;
import name.pachler.nio.file.WatchEvent;
import name.pachler.nio.file.WatchKey;
import name.pachler.nio.file.WatchService;
import name.pachler.nio.file.WatchEvent.Kind;

import org.apache.commons.io.FilenameUtils;

import com.pic.mgt.log.IgxLog;
import com.pic.mgt.utils.LogUtils;

/**
 * [檔案監控實作] 採用JPatchWatch，適用於 FreeBSD-i386, GNU-Linux-x86, 
 * GNU-MacOSX, MinGW-Windows。支援監控事件類型：FILE_Generate,
 * FILE_CREATE,FILE_DELETE,FILE_MOTIFY。
 * @author 洪政緯
 */
public class JPathWatchFileMonitor implements FileMonitor {
	// 監控相關設定
	private FileMonitorStatus monitorStatus = FileMonitorStatus.MONITOR_INITIAL;
	private List<FilenameFilter> filenameFilterList;
	private Map<String, MonitorPathInfo> monitorPathInfoMap;
	private Map<WatchKey, String> monitorPathStrMap;
	private Vector<FileMonitorEvent> fileMonitorEventsVector;
	private Kind[] eventKinds;
	
	private WatchService watchService;
	
	public JPathWatchFileMonitor() {
		fileMonitorEventsVector = new Vector<FileMonitorEvent>();	
		filenameFilterList = new LinkedList<FilenameFilter>();
		monitorPathInfoMap = new HashMap<String, MonitorPathInfo>();
		monitorPathStrMap = new HashMap<WatchKey, String>();
		
		// 建立起一個 WatchService後,將之關閉(不做任何監控)。
		watchService = FileSystems.getDefault().newWatchService();
		try {
			watchService.close();
		} catch (IOException e) {
			// 不需做任何處理
		}
	}

	@Override
	public synchronized FileMonitorStatus getMonitorStatus() {
		return this.monitorStatus;
	}

	@Override
	public List<MonitoredFile> readMonitoredList()
									throws FileMonitorStopException {
		if (!this.monitorStatus.equals(FileMonitorStatus.MONITOR_STARTED)) {
			throw new FileMonitorStopException(
					"File monitor has not started yet.");
		}
		
		List<MonitoredFile> monitoredFileList =
							new LinkedList<MonitoredFile>();
		WatchKey watchKey;
		try {
			// throw InterruptedException
			watchKey = watchService.take(); // 程式會停在此，一直到有得到 WatchKey
			for (WatchEvent<?> event: watchKey.pollEvents()){
				@SuppressWarnings(value={"unchecked"})
				WatchEvent<Path> watchEvent = (WatchEvent<Path>)event;
				
				// 如果是 Overflow 事件，則必須寫 log 警告
				// ********************** Log **********************
				if (watchEvent.kind().equals(StandardWatchEventKind.OVERFLOW)) {
					IgxLog.messageDetailsLog.error(this, "An file monitor overflow event occured."
							+ " Some of files motion maybe lost.");
				}
				// ********************** Log **********************
				
				/*
				 * 監控到的檔名(可能為null),當 Event Kind 為 OVERFLOW時，
				 * 檔名就會為 null。
				 */
				Path filename = watchEvent.context();
				
				File watchedFile = null;
				if (filename != null) {
					// ********************** Log **********************
					if (IgxLog.messageDetailsLog.isInfoMode()) {
						IgxLog.messageDetailsLog.info(this, "Monitored a file motion. Filename: '{}'  Event: '{}'",
								filename, watchEvent.kind().name());
					}
					// ********************** Log **********************
					
					// 轉為 File Object
					watchedFile = new File(
							FilenameUtils.concat(
									this.monitorPathStrMap.get(watchKey),
									filename.toString()));
					
					// 如果使用 file name filter, 則呼叫 filter進行檔名過濾
					if (!this.filenameFilterList.isEmpty()) {
						boolean acceptable = false; 
						synchronized (this.filenameFilterList) {
							for (FilenameFilter filenameFilter : this.filenameFilterList) {
								if (filenameFilter.
										 accept(watchedFile, watchedFile.getName())) {
									acceptable = true;
									break;
								}
							}
						}
						if (!acceptable) {
							// ********************** Log **********************
							if (IgxLog.messageDetailsLog.isInfoMode()) {
								IgxLog.messageDetailsLog.info(this, "'{}' has been ignored by filter.", filename);
							}
							// ********************** Log **********************
							
							continue;
						}
					}
				}
                
				// 取得事件類型(將 JPathWatch Event 對應成 FileMonitor Event)
				Kind<Path> eventKind = watchEvent.kind(); 
				FileMonitorEvent eventType = null;
				if (eventKind.equals(StandardWatchEventKind.ENTRY_CREATE)) {
					/* FileMonitorEvent.File_Generate 於本實作中，對應到的是 ENTRY_CREATE 事件，
					 * 若叫用端有採用 FileMonitorEvent.File_Generate 事件，以其為優先。
					 */
					if (this.fileMonitorEventsVector.contains(
										FileMonitorEvent.FILE_Generate)) {
						eventType = FileMonitorEvent.FILE_Generate;
					}
					else {
						eventType = FileMonitorEvent.FILE_CREATE;
					}
				}
				else if (eventKind.equals(StandardWatchEventKind.ENTRY_DELETE)) {
					eventType = FileMonitorEvent.FILE_DELETE;
				}
				else if (eventKind.equals(StandardWatchEventKind.ENTRY_MODIFY)) {
					eventType = FileMonitorEvent.FILE_MOTIFY;
				}
				else if (eventKind.equals(StandardWatchEventKind.OVERFLOW)) {
					eventType = FileMonitorEvent.EVENTOVERFLOW;
				}
				monitoredFileList.add(
						new MonitoredFile(watchedFile.toString(), eventType));				
			}
			watchKey.reset();
		} catch (InterruptedException e1) {
			// 不明原因被中斷
			try {
				throw new FileMonitorStopException("File monitor has been stopped. Because of unknown reason.", e1);
			} catch (FileMonitorStopException e) {
				// ********************** Log **********************
				LogUtils.error(this, IgxLog.messageDetailsLog, e);
				// ********************** Log **********************
				
				throw e;
			}
		} catch (ClosedWatchServiceException e2) { 
			// 這因為是呼叫了 stopMonitor() ,所以造成此例外是正常的。
			try {
				throw new FileMonitorStopException(
						"File monitor has been stopped. Because of watch service is closed.", e2);
			} catch (FileMonitorStopException e) {
				// ********************** Log **********************
				if (IgxLog.messageDetailsLog.isDebugMode()) {
					LogUtils.debug(this, IgxLog.messageDetailsLog, e);
				}
				// ********************** Log **********************
				
				throw e;
			}
		}
		
		return monitoredFileList;
	}

	@Override
	public synchronized void addFilenameFilter(FilenameFilter filenameFilter) {
		// file filter 不能在 monitor started 情況下改變
		if (this.monitorStatus != FileMonitorStatus.MONITOR_STARTED) {
			synchronized (this.filenameFilterList) {
				this.filenameFilterList.add(filenameFilter);
			}
		}		
	}
	
	@Override
	public synchronized void removeAllFilenameFilter() {
		// file filter 不能在 monitor started 情況下改變
		if (this.monitorStatus != FileMonitorStatus.MONITOR_STARTED) {
			synchronized (this.filenameFilterList) {
				this.filenameFilterList.clear();
			}
		}
	}

	@Override
	public synchronized void addMonitorPath(String monitorPathStr) throws FileMonitorException {
		// 如果監控目錄不在在，則拋出例外
		if (!(new File(monitorPathStr)).exists()) {
			throw new FileMonitorException("The monitor path '" + 
										monitorPathStr + "' does not exist.");
		}
		
		// 要避免重覆加入以至 MonitorPathInfo被覆蓋掉
		if (!this.monitorPathInfoMap.containsKey(monitorPathStr)) {
			this.monitorPathInfoMap.put(monitorPathStr,
										new MonitorPathInfo(monitorPathStr,
															null));
		}
		
		// ********************** Log **********************
		if (IgxLog.messageDetailsLog.isDebugMode()) {
			IgxLog.messageDetailsLog.debug(this,
					"Added a folder monitor path '{}'.", monitorPathStr);
		}
		// ********************** Log **********************
		
		
		// 如果是已經開始監控的狀態下,則直接將此 monitorPaht 加入 JPatchWatch 的 watch，開始監控
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			// 如果該Path還沒有被監控才加入監控,反之則不需再做一次
			MonitorPathInfo monitorPathInfo = this.monitorPathInfoMap.get(monitorPathStr);
			if (null == monitorPathInfo.getJPathWatchKey()) {
				// throws FileMonitorException
				startWatchMonitorPath(monitorPathStr);
			}
		}
	}
	
	private WatchKey addJPathWatch(String monitorPath) throws IOException {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			if (null != this.watchService && !this.fileMonitorEventsVector.isEmpty() && null != monitorPath) {
				// throws InotifyException
				//檔 register 完成時，即開始監控程序。
				WatchKey watchKey =
						Paths.get(monitorPath).register(this.watchService, this.eventKinds);
				
				return watchKey;
			}
		}
		
		return null;
	}
	
	@Override
	public synchronized void removeMonitorPath(String monitorPathStr) {
		// 如果已經在監控的狀態下,必須先停止JPathWatch 的 watch
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			stopWatchMonitorPath(monitorPathStr);
		}
		
		this.monitorPathInfoMap.remove(monitorPathStr);
	}
	
	@Override
	public synchronized void removeAllMonitorPath() {
		Set<String> monitorPathStrSet = this.monitorPathInfoMap.keySet();
		for (String monitorPathStr : monitorPathStrSet) {
			removeMonitorPath(monitorPathStr);
		}
	}
	
	@Override
	public synchronized void stopWatchAllMonitorPath() {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			if (null != this.watchService) {
				Set<String> monitorPathStrSet = this.monitorPathInfoMap.keySet();
				for (String monitorPathStr : monitorPathStrSet) {
					stopWatchMonitorPath(monitorPathStr);
				}
			}
		}
	}
	
	@Override
	public synchronized void stopWatchMonitorPath(String monitorPathStr) {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			MonitorPathInfo monitorPathInfo = this.monitorPathInfoMap.get(monitorPathStr);
			if (null != monitorPathInfo) {
				WatchKey watchKey = monitorPathInfo.getJPathWatchKey();
				if (null != watchKey) {
					// 若 watchKey為null,表示該目錄並沒有在被監控中
					watchKey.cancel();
					
					monitorPathInfo.setJPathWatchKey(null);
					
					this.monitorPathStrMap.remove(watchKey);
				}
			}
		}
	}
	
	@Override
	public synchronized void setMonitorEvent(FileMonitorEvent... events) {
		// monitor path 不能在 monitor started 情況下改變
		if (this.monitorStatus != FileMonitorStatus.MONITOR_STARTED) {
			fileMonitorEventsVector.clear();
			for(FileMonitorEvent event : events) {
				this.fileMonitorEventsVector.add(event);
			}	
		}
	}

	@Override
	public synchronized void startMonitor() throws FileMonitorException {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			return;
		}
		
		try {
			// 如果尚未設定監控目錄和事件，則拋出例外
			if (this.monitorPathInfoMap.isEmpty() || this.fileMonitorEventsVector.isEmpty()) {
				throw new FileMonitorException("Monitor path and events are not provided.");
			}
		} catch (FileMonitorException e1) {
			// ********************** Log **********************
			IgxLog.messageDetailsLog.error(this, e1.getMessage());
			// ********************** Log **********************
			
			throw e1;
		}
		
		// 取得轉換後的事件類型
		this.eventKinds = mappingEvents();
		if (eventKinds == null) {
			throw new FileMonitorException("do not find any match event kinds.");
		}
		
		// 建立一個新的 WatchService (當建立起 WatchService即已處於監控的狀態)
		this.watchService = FileSystems.getDefault().newWatchService();
		
		this.monitorStatus = FileMonitorStatus.MONITOR_STARTED;
		
		// ********************** Log **********************
		if (IgxLog.messageDetailsLog.isInfoMode()) {
			IgxLog.messageDetailsLog.info(this, "Started file monitor.");
		}
		// ********************** Log **********************
		
		
		// 將Map內的monitor path 全部設定/增加到JPatchWatch路徑監控
		Set<String> monitorPathStrSet = this.monitorPathInfoMap.keySet();
		for (String monitorPathStr : monitorPathStrSet) {
			// throws FileMonitorException
			startWatchMonitorPath(monitorPathStr);
		}
	}
	
	@Override
	public synchronized void startWatchMonitorPath(String monitorPathStr) throws FileMonitorException {
		if (this.monitorStatus == FileMonitorStatus.MONITOR_STARTED) {
			MonitorPathInfo monitorPathInfo = null;
			try {
				monitorPathInfo = this.monitorPathInfoMap.get(monitorPathStr);
				if (null != monitorPathInfo) {
					// throws IOException
					WatchKey watchKey = addJPathWatch(monitorPathInfo.getMonitorPathStr());
					
					monitorPathInfo.setJPathWatchKey(watchKey);
					
					this.monitorPathStrMap.put(watchKey, monitorPathStr);
				}
			} catch (IOException e) {
				try {
					throw new FileMonitorException(
							"Adding monitor path '" + monitorPathInfo.getMonitorPathStr() +
							"' watch error.", e);
				} catch (FileMonitorException e1) {
					// ********************** Log **********************
					LogUtils.error(this, IgxLog.messageDetailsLog, e1);
					// ********************** Log **********************
					
					throw e1;
				}
			}
		}
	}
	
	/**
	 * 
	 * @return
	 */
	@SuppressWarnings(value={"unchecked"})
	private Kind[] mappingEvents() {		
		List<Kind> kindList = new LinkedList<Kind>();
		Iterator<FileMonitorEvent> iter = this.fileMonitorEventsVector.iterator();
		while(iter.hasNext()){
			FileMonitorEvent tmp = iter.next();
			switch(tmp){
				case FILE_Generate:
					// FILE_Generate 於本實作中對應到 ENTRY_CREATE
					kindList.add(StandardWatchEventKind.ENTRY_CREATE);
					break;
				case FILE_CREATE:
					kindList.add(StandardWatchEventKind.ENTRY_CREATE);
					break;
				case FILE_DELETE:
					kindList.add(StandardWatchEventKind.ENTRY_DELETE);
					break;
				case FILE_MOTIFY:
					kindList.add(StandardWatchEventKind.ENTRY_MODIFY);
					break;
			}
		}
		if (kindList.isEmpty()) {
			return null;
		}
		else {
			@SuppressWarnings(value={"unchecked"})
			Kind[] eventKinds = new Kind[kindList.size()];
			kindList.toArray(eventKinds);
			return eventKinds;
		}		
	}

	@Override
	public synchronized void stopMonitor() {
		if (!this.monitorStatus.equals(FileMonitorStatus.MONITOR_STARTED)) {
			return;
		}
		
		if (watchService != null) {
			stopWatchAllMonitorPath();

			try {
				// throw IOException
				watchService.close();
			} catch (IOException e) {
				try {
					throw new FileMonitorException(
							"Closeing watch service error.",e);
				} catch (FileMonitorException e1) {
					// ********************** Log **********************
					if (IgxLog.messageDetailsLog.isWarnMode()) {
						LogUtils.warn(this, IgxLog.messageDetailsLog, e1);
					}
					// ********************** Log **********************
				}
			} finally {
				this.monitorStatus = FileMonitorStatus.MONITOR_STOPPED;
				
				// ********************** Log **********************
				if (IgxLog.messageDetailsLog.isInfoMode()) {
					IgxLog.messageDetailsLog.info(this, "Stopped file monitor.");
				}
				// ********************** Log **********************
			}
		}
	}
	
	
	private class MonitorPathInfo {
		private String monitorPathStr;
		private WatchKey watchKey;
		
		private MonitorPathInfo(String monitorPath) {
			this.monitorPathStr = monitorPath;
		}
		
		private MonitorPathInfo(String monitorPath, WatchKey watchKey) {
			this.watchKey = watchKey;
			this.monitorPathStr = monitorPath;
		}
		
		// --- getter and setter ---

		public String getMonitorPathStr() {
			return monitorPathStr;
		}

		public void setMonitorPathStr(String monitorPathStr) {
			this.monitorPathStr = monitorPathStr;
		}

		public WatchKey getJPathWatchKey() {
			return watchKey;
		}

		public void setJPathWatchKey(WatchKey watchKey) {
			this.watchKey = watchKey;
		}
	}

}
